ひとりNavigation API Advent Calendar 15日目
https://gyazo.com/cc571bdfa0e21efa77b607e6297ea44b
これはひとりNavigation API Advent Calendarの15日目です。
今日はVue RouterをNavigation APIにして行こうとしている取り組みを見ていきます
GitHub - userquin/vue-router-api
feat: add navigation-api router by userquin · Pull Request #2551 · vuejs/router · GitHub
Vue Router本体への実装PR
navigationApiHistory.tsに処理がある
code:ts
import { createNavigationApiHistory } from './navigationApiHistory.ts'
code:ts
const history = window.navigation
? createNavigationApiHistory()
: createWebHistory()
const router = createRouter({
history,
routes,
})
Navigation APIが使えるか・そうじゃないかでcreateNavigationApiHistory()を呼び出している
Vue RouterのcreateWebHistoryと比較していこう
https://router.vuejs.org/api/functions/createWebHistory.html
History APIを使う君
https://github.com/vuejs/router/blob/3e3efef8cb98add55302e1c944f45b4a245212c5/packages/router/src/history/html5.ts#L317C1-L356C2
code:ts
export function createWebHistory(base?: string): RouterHistory {
base = normalizeBase(base)
const historyNavigation = useHistoryStateNavigation(base)
const historyListeners = useHistoryListeners(
base,
historyNavigation.state,
historyNavigation.location,
historyNavigation.replace
)
function go(delta: number, triggerListeners = true) {
if (!triggerListeners) historyListeners.pauseListeners()
history.go(delta)
}
const routerHistory: RouterHistory = assign(
{
// it's overridden right after
location: '',
base,
go,
createHref: createHref.bind(null, base),
},
historyNavigation,
historyListeners
)
Object.defineProperty(routerHistory, 'location', {
enumerable: true,
get: () => historyNavigation.location.value,
})
Object.defineProperty(routerHistory, 'state', {
enumerable: true,
get: () => historyNavigation.state.value,
})
return routerHistory
}
useHistoryStateNavigationはpush/replaceの実装
pushメソッドは、ブラウザのhistory.pushState()を呼び出して新しい履歴エントリを追加する
内部的には、changeLocation関数がHistory APIのpushStateまたはreplaceStateを実際に呼び出す
code:ts
function changeLocation(
to: HistoryLocation,
state: StateEntry,
replace: boolean
): void {
/**
* if a base tag is provided, and we are on a normal domain, we have to
* respect the provided base attribute because pushState() will use it and
* potentially erase anything before the # like at
* https://github.com/vuejs/router/issues/685 where a base of
* /folder/# but a base of / would erase the /folder/ section. If
* there is no host, the <base> tag makes no sense and if there isn't a
* base tag we can just use everything after the #.
*/
const hashIndex = base.indexOf('#')
const url =
hashIndex > -1
? (location.host && document.querySelector('base')
? base
: base.slice(hashIndex)) + to
: createBaseLocation() + base + to
try {
// BROWSER QUIRK
// NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
historyreplace ? 'replaceState' : 'pushState'(state, '', url)
historyState.value = state
} catch (err) {
if (__DEV__) {
warn('Error with push/replace State', err)
} else {
console.error(err)
}
// Force the navigation, this also resets the call count
locationreplace ? 'replace' : 'assign'(url)
}
}
NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
ここでも見かけた
ひとりNavigation API Advent Calendar 08日目#69367aa300000000008fe439
エラーが発生した場合はlocation.assign()やlocation.replace()にフォールバックする
useHistoryListenersはpopstateイベントの管理
popStateHandlerが実装されており、ユーザーがブラウザの戻る/進むボタンを押すと
1. 現在の位置を計算
2. 履歴の状態を更新
3. 登録されたすべてのリスナーに通知
の順番で処理を行う
code:3の処理.ts
listeners.forEach(listener => {
listener(currentLocation.value, from, {
delta,
type: NavigationType.pop,
direction: delta
? delta > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
})
})
Vue Routerは、History APIの状態に独自のStateEntryオブジェクトを保存する
HistoryStateの型をextendsしている
code:ts
export type HistoryStateValue =
| string
| number
| boolean
| null
| undefined
| HistoryState
| HistoryStateArray
export interface HistoryState {
x: number: HistoryStateValue
x: string: HistoryStateValue
}
このオブジェクトには以下の情報が含まれる
back: 前の履歴位置
current: 現在の履歴位置
forward: 次の履歴位置
position: 履歴内の位置番号
replaced: replaceStateで置き換えられたかどうか
scroll: スクロール位置
ページが非表示になる際(pagehideイベントやvisibilitychangeイベント)に、現在のスクロール位置を履歴の状態に保存してリスナーに登録している
code:ts
function beforeUnloadListener() {
if (document.visibilityState === 'hidden') {
const { history } = window
if (!history.state) return
history.replaceState(
assign({}, history.state, { scroll: computeScrollPosition() }),
''
)
}
}
code:ts
// https://developer.chrome.com/blog/page-lifecycle-api/
// note: iOS safari does not fire beforeunload, so we
// use pagehide and visibilitychange instead
window.addEventListener('pagehide', beforeUnloadListener)
document.addEventListener('visibilitychange', beforeUnloadListener)
Page Lifecycle API | Web Platform | Chrome for Developers
https://gyazo.com/4cf2c46cdf5b1ceee06e904e4b803f40
覚えられん…
ところで
Vue Routerのメモリーモードって存在知らんかった
Node.js環境やSSRで使うらしいが使い方が想像できない
While it's not recommended, you can use this mode inside Browser applications but note there will be no history, meaning you won't be able to go back or forward.
推奨はされませんが、このモードをブラウザアプリケーション内で使用することは可能です。ただし、履歴は保存されないため、戻る/進む操作はできません。
どういうこと...?
Storybook上でのVue Router表現で使うというのは見かけた
https://zenn.dev/sa2knight/books/storybook-7-with-vue-3/viewer/vue-router
Navigation API版を見ていく
vue-router-api/src/navigationApiHistory.ts at nav-api-vue-router · userquin/vue-router-api · GitHub
code:ts
if (event.navigationType === 'traverse') {
const fromIndex = window.navigation.currentEntry?.index ?? -1
const toIndex = event.destination.index
const delta = fromIndex === -1 ? 0 : toIndex - fromIndex
info = {
type: 'pop', // 'traverse' maps to 'pop' in vue-router's terminology.
direction: delta > 0 ? 'forward' : 'back',
delta,
}
}
else if (event.navigationType === 'push' || event.navigationType === 'replace') {
info = {
type: event.navigationType,
direction: '', // No specific direction for push/replace.
delta: event.navigationType === 'push' ? 1 : 0,
}
}
else {
// For 'reload' or other types, we ignore and don't notify listeners.
return
}
navigationTypeにあわせた処理
code:ts
const url = new URL(event.destination.url)
const to = url.pathname + url.search + url.hash
const from = location.value
// We intercept the navigation so vue-router can handle the view update.
event.intercept({
handler: async () => {
location.value = to
state.value = event.destination.getState() as HistoryState
// Notify vue-router listeners with the enriched navigation info.
listeners.forEach(listener => listener(to, from, info))
},
})
code:ts
window.navigation.addEventListener('navigate', handleNavigate)
push、replace、go、listen、destroyメソッドをもつhistoryオブジェクト
RouterHistory | Vue Routerに沿った形で一部Navigation APIで代替
code:ts
push(to: string, data?: HistoryState) {
window.navigation.navigate(to, { state: data, history: 'push' })
},
replace(to: string, data?: HistoryState) {
window.navigation.navigate(to, { state: data, history: 'replace' })
},
code:ts
go(delta: number) {
// Case 1: go(0) should trigger a reload.
if (delta === 0) {
window.navigation.reload()
return
}
// Get the current state safely, without using non-null assertions ('!').
const entries = window.navigation.entries()
const currentIndex = window.navigation.currentEntry?.index
// If we don't have a current index, we can't proceed.
if (currentIndex === undefined) {
return
}
// Calculate the target index in the history stack.
const targetIndex = currentIndex + delta
// Validate that the target index is within the bounds of the entries array.
// This is the key check that prevents runtime errors.
if (targetIndex >= 0 && targetIndex < entries.length) {
// Each history entry has a unique 'key'. We get the key for our target entry...
// Safely get the target entry from the array.
const targetEntry = entriestargetIndex
// Add a check to ensure the entry is not undefined before accessing its key.
// This satisfies TypeScript's strict checks.
if (targetEntry) {
window.navigation.traverseTo(targetEntry.key)
}
else {
// This case is unlikely if the index check passed, but it adds robustness.
console.warn(go(${delta}) failed: No entry found at index ${targetIndex}.)
}
}
else {
console.warn(go(${delta}) failed: target index ${targetIndex} is out of bounds.)
}
},
feat: add navigation-api router by userquin · Pull Request #2551 · vuejs/router · GitHub
Vue Router移植編は明日にしようかな…
https://gyazo.com/3a7fd7f66965d137e541e0e94355baa2
History APIのをレガシーと称してそうなのは見かけた